/*
 *  linux/kernel/floppy.c
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 * 02.12.91 - Changed to static variables to indicate need for reset
 * and recalibrate. This makes some things easier (output_byte reset
 * checking etc), and means less interrupt jumping in case of errors,
 * so the code is hopefully easier to understand.
 */

/*
 * This file is certainly a mess. I've tried my best to get it working,
 * but I don't like programming floppies, and I have only one anyway.
 * Urgel. I should check for more errors, and do more graceful error
 * recovery. Seems there are problems with several drives. I've tried to
 * correct them. No promises. 
 */

/*
 * As with hd.c, all routines within this file can (and will) be called
 * by interrupts, so extreme caution is needed. A hardware interrupt
 * handler may not sleep, or a kernel panic will happen. Thus I cannot
 * call "floppy-on" directly, but have to set a special timer interrupt
 * etc.
 *
 * Also, I'm not certain this works on more than 1 floppy. Bugs may
 * abund.
 */

#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/fdreg.h>
#include <asm/system.h>
#include <asm/io.h>
#include <asm/segment.h>

#define MAJOR_NR 2		// 软驱的主设备号是2。
#include "blk.h"		// 块设备头文件。定义请求数据结构、块设备数据结构和宏函数等信息。

static int recalibrate = 0;	// 标志：需要重新校正。
static int reset = 0;		// 标志：需要进行复位操作。
static int seek = 0;		// 是否需要寻道。

// 对应软驱控制器中当前的 数字输出寄存器（DOR）。
// 该寄存器每位的定义如下：
// 位7-4：分别控制驱动器 D-A 马达的启动。1- 启动：0-关闭。
// 位3：1-允许 DMA 和中断请求；0-禁止 DMA 和中断请求。
// 位2：1- 启动软盘控制器；0一复位软盘控制器。
// 位1-0：00-11，用于选择控制的软驱 A-D。
extern unsigned char current_DOR;	// 当前数字输出寄存器(Digital Output Register)。

// 字节直接输出（把值 val 输出到 port 端口）。
#define immoutb_p(val,port) \
__asm__("outb %0,%1\n\tjmp 1f\n1:\tjmp 1f\n1:"::"a" ((char) (val)),"i" (port))

// 这两个定义用于计算软驱的设备号。次设备号 = TYPE*4 + DRIVE。计算方法参见列表后。
#define TYPE(x) ((x)>>2)	// 软驱类型（2--1.2Mb，7--1.44Mb）。
#define DRIVE(x) ((x)&0x03)	// 软驱序号（0--3 对应A--D）。
/*
 * Note that MAX_ERRORS=8 doesn't imply that we retry every bad read
 * max 8 times - some types of errors increase the errorcount by 2,
 * so we might actually retry only 5-6 times before giving up.
 */
/*
* 注意，下面定义MAX_ERRORS=8 并不表示对每次读错误尝试最多8 次 - 有些类型
* 的错误将把出错计数值乘2，所以我们实际上在放弃操作之前只需尝试5-6 遍即可。
*/
#define MAX_ERRORS 8

/*
 * globals used by 'result()'
 */
/* 下面是函数'result()'使用的全局变量 */
// 这些状态字节中各比特位的含义请参见include/linux/fdreg.h 头文件。
#define MAX_REPLIES 7		// FDC 最多返回7 字节的结果信息。

static unsigned char reply_buffer[MAX_REPLIES];  // 存放FDC 返回的结果信息。

#define ST0 (reply_buffer[0])	// 返回结果状态字节0。
#define ST1 (reply_buffer[1])	// 返回结果状态字节1。
#define ST2 (reply_buffer[2])	// 返回结果状态字节2。
#define ST3 (reply_buffer[3])	// 返回结果状态字节3。

/*
 * This struct defines the different floppy types. Unlike minix
 * linux doesn't have a "search for right type"-type, as the code
 * for that is convoluted and weird. I've got enough problems with
 * this driver as it is.
 *
 * The 'stretch' tells if the tracks need to be boubled for some
 * types (ie 360kB diskette in 1.2MB drive etc). Others should
 * be self-explanatory.
 */
/*
* 下面的软盘结构定义了不同的软盘类型。与minix 不同的是，linux 没有
* "搜索正确的类型"-类型，因为对其处理的代码令人费解且怪怪的。本程序
* 已经让我遇到了许多的问题了。
*
* 对某些类型的软盘（例如在1.2MB 驱动器中的360kB 软盘等），'stretch'用于
* 检测磁道是否需要特殊处理。其它参数应该是自明的。
*/
// 软盘参数有：
// size 大小(扇区数)；
// sect 每磁道扇区数；
// head 磁头数；
// track 磁道数；
// stretch 对磁道是否要特殊处理（标志）；
// gap 扇区间隙长度(字节数)；
// rate 数据传输速率；
// spec1 参数（高4 位步进速率，低四位磁头卸载时间）。
static struct floppy_struct {
	unsigned int size, sect, head, track, stretch;
	unsigned char gap,rate,spec1;
} floppy_type[] = {
	{    0, 0,0, 0,0,0x00,0x00,0x00 },	/* no testing */
	{  720, 9,2,40,0,0x2A,0x02,0xDF },	/* 360kB PC diskettes */
	{ 2400,15,2,80,0,0x1B,0x00,0xDF },	/* 1.2 MB AT-diskettes */
	{  720, 9,2,40,1,0x2A,0x02,0xDF },	/* 360kB in 720kB drive */
	{ 1440, 9,2,80,0,0x2A,0x02,0xDF },	/* 3.5" 720kB diskette */
	{  720, 9,2,40,1,0x23,0x01,0xDF },	/* 360kB in 1.2MB drive */
	{ 1440, 9,2,80,0,0x23,0x01,0xDF },	/* 720kB in 1.2MB drive */
	{ 2880,18,2,80,0,0x1B,0x00,0xCF },	/* 1.44MB diskette */
};
/*
 * Rate is 0 for 500kb/s, 2 for 300kbps, 1 for 250kbps
 * Spec1 is 0xSH, where S is stepping rate (F=1ms, E=2ms, D=3ms etc),
 * H is head unload time (1=16ms, 2=32ms, etc)
 *
 * Spec2 is (HLD<<1 | ND), where HLD is head load time (1=2ms, 2=4 ms etc)
 * and ND is set means no DMA. Hardcoded to 6 (HLD=6ms, use DMA).
 */
/*
* 上面速率rate：0 表示500kb/s，1 表示300kbps，2 表示250kbps。
* 参数spec1 是0xSH，其中S 是步进速率（F-1 毫秒，E-2ms，D=3ms 等），
* H 是磁头卸载时间（1=16ms，2=32ms 等）
*
* spec2 是（HLD<<1 | ND），其中HLD 是磁头加载时间（1=2ms，2=4ms 等）
* ND 置位表示不使用DMA（No DMA），在程序中硬编码成6（HLD=6ms，使用DMA）。
*/

extern void floppy_interrupt(void);
// 这是 boot/head.s 中定义的临时软盘缓冲区。
// 如果请求项的缓冲区处于内存 1MB 以上某个地方，则需要将 DMA 缓冲区设在临时缓冲区域处。因为 8237A芯片只能在 1MB 地址范围内寻址。
extern char tmp_floppy_area[1024];

/*
 * These are global variables, as that's the easiest way to give
 * information to interrupts. They are the data used for the current
 * request.
 */

/*
* 下面是一些全局变量
因为这是将信息传给中断程序最简单的方式。它们是用于当前请求的数据。
*/
static int cur_spec1 = -1;				  // 当前软盘参数 spec1。
static int cur_rate = -1;                 // 当前软盘转速 rate。
static struct floppy_struct * floppy = floppy_type;
static unsigned char current_drive = 0;    // 当前驱动器号（取值为0-3，表示软驱A-D）。
static unsigned char sector = 0;           // 当前扇区号
static unsigned char head = 0;             // 当前磁头号
static unsigned char track = 0;            // 当前磁道号
static unsigned char seek_track = 0;       // 寻道磁道号。
static unsigned char current_track = 255;  // 当前磁头所在磁道号（柱面号）。
static unsigned char command = 0;          // 读写操作的命令
unsigned char selected = 0;		// 软驱己选定标志。在处理请求项之前要首先选定软驱。
struct task_struct * wait_on_floppy_select = NULL;  // 等待选定软驱的任务队列（直到有软驱别选定）。

// 释放（取消选定的）软驱。
// 实现：如果函数参数指定的软驱 nr 当前并没有被选定，则显示警告信息。然后复位软驱已选定标志 selected，并唤醒等待选择该软驱的任务。
// 数字输出寄存器(DOR)的低2 位用于指定选择的软驱（0-3 对应A-D）。
void floppy_deselect(unsigned int nr)
{
	if (nr != (current_DOR & 3))
		printk("floppy_deselect: drive not selected\n\r");
	selected = 0;
	wake_up(&wait_on_floppy_select);
}

/*
 * floppy-change is never called from an interrupt, so we can relax a bit
 * here, sleep etc. Note that floppy-on tries to set current_DOR to point
 * to the desired drive, but it will probably not survive the sleep if
 * several floppies are used at the same time: thus the loop.
 */

// 检测指定软驱中软盘更换情况。如果软盘更换了，则返回1，否则返回0。
// 参数 nr 是软驱号。
// 该函数首先选定参数指定的软驱 nr，然后测试软盘控制器的数字输入寄存器 DIR 的值，以判断驱动器中的软盘是否被更换过。
// 该函数由程序 fs/buffer.c 中的 check_disk_change() 函数调用（第119行)。
int floppy_change(unsigned int nr)
{
repeat:
	// 开启指定软驱的马达
	floppy_on(nr);
	// 如果已经有选择的软驱但选择的软驱不是指定的软驱，即选择了其他软驱，则睡眠，直到软驱选定释放（可以重新选择软驱）时被唤醒
	while ((current_DOR & 3) != nr && selected)
		interruptible_sleep_on(&wait_on_floppy_select);
	// 当selected = 0 或被唤醒时，当前软驱仍不是指定的软驱nr，则重新循环等待
	if ((current_DOR & 3) != nr)
		goto repeat;
	// 现在软盘控制器已选定我们指定的软驱 nr
	// 于是取数字输入寄存器 DIR 的值，如果其最高位（位7）置位，则表示驱动器中的软盘己被更换，此时即可关闭马达，并返回1退出。
	if (inb(FD_DIR) & 0x80) {
		floppy_off(nr);
		return 1;
	}
	// 否则关闭马达，并返回0退出。表示磁盘没有被更换。
	floppy_off(nr);
	return 0;
}

// 复制内存缓冲块，共1024字节。
// 从内存地址 from 处复制 1024字节数据到地址 to 处。
#define copy_buffer(from,to) \
__asm__("cld ; rep ; movsl" \
	::"c" (BLOCK_SIZE/4),"S" ((long)(from)),"D" ((long)(to)) \
	)

// 设置（初始化）软盘 DMA 通道。
// 软盘中数据读写操作是使用 DMA 进行的。因此在每次进行数据传输之前需要设置 DMA 芯片上专门用于软驱的通道2。有关 DMA 编程方法请参见程序列表后的信息。
static void setup_DMA(void)
{
	// 1、设置DMA操作的目标地址
	long addr = (long) CURRENT->buffer;  // 当前请求项缓冲区所处内存地址。

	cli();
	// 首先检测请求项的缓冲区所在位置。
	// 如果缓冲区处于内存 1MB 以上的某个地方，
	// 则需要将 DMA 缓冲区设在临时缓冲区域（tmp_f1oppy_area）处。因为8237A芯片只能在 1MB 地址范围内寻址。补充：受到硬件方面的限制制约，比如在 X86 体系结构下，ISA 总线的 DMA控制器，只能对内存的前16M 进行寻址，这就导致了 ISA 设备不能在整个 32 位地址空间中执行 DMA，只能使用物理内存的前 16M 进行 DMA 操作。
	// 如果是写盘命令，则还需要把数据从请求项缓冲区复制到该临时区域。
	if (addr >= 0x100000) {
		addr = (long) tmp_floppy_area;
		if (command == FD_WRITE)
			copy_buffer(CURRENT->buffer,tmp_floppy_area);
	}
	// 2、接下来我们开始设置 DMA 通道2。
	/*
	在开始设置之前需要先屏蔽该通道。单通道屏蔽寄存器端口为 Ox0A。位0-1指定 DMA 通道(0--3)，位2：1 表示屏蔽，0表示允许请求。
	然后向 DMA 控制器端口 12 和 11 写入方式字（读盘是0x46，写盘则是 0x4A）。
	再写入传输使用缓冲区地址 addr 和需要传输的字节数 0x3ff(0--1023）。
	最后复位对 DMA 通道2 的屏蔽， 开放 DMA2 请求 DREQ 信号。
	*/
/* mask DMA 2 */
	immoutb_p(4|2,10);
/* output command byte. I don't know why, but everyone (minix, */
/* sanches & canton) output this twice, first to 12 then to 11 */
	/*
	下面嵌入汇编代码向 DMA 控制器的“清除先后触发器”端口12 和方式寄存器端口11写入方式字（读盘时端口是0x46，写盘是 Ox4A)。

	由于各通道的地址和计数寄存器都是16 位的，因此在设置他们时都需要分2 次进行操作。
	一次访问低字节，另一次访问高字节。而实际在写哪个字节则由"先后触发器"的状态决定。
	当"先后触发器"为0时，则访问低字节；当字节触发器为1时，则访问高字节。每访问一次， 该触发器的状态就变化一次。
	而写端口12 就可以将触发器置成0状态，从而对 16 位寄存器的设置从低字节开始。
	 */
 	__asm__("outb %%al,$12\n\tjmp 1f\n1:\tjmp 1f\n1:\t"
	"outb %%al,$11\n\tjmp 1f\n1:\tjmp 1f\n1:"::
	"a" ((char) ((command == FD_READ)?DMA_READ:DMA_WRITE)));
/* 8 low bits of addr */
	// 向 DMA通道2写入基/当前地址寄存器（端口4）
	immoutb_p(addr,4);
	addr >>= 8;
/* bits 8-15 of addr */ /*地址高8-15位 */
	immoutb_p(addr,4);
	addr >>= 8;
/* bits 16-19 of addr */ /*地址高16-19位 */
	// DMA 只可以在 1MB 内存空间内寻址，其高16-19 位地址需放入页面寄存器(端口 0×81)。
	immoutb_p(addr,0x81);
// 向 DMA 通道2写入基/当前字节计数器值（端口5），一次共传输1024字节。
/* low 8 bits of count-1 (1024-1=0x3ff) */ /* 计数器低8位(1024-1 = 0x3ff） */
	immoutb_p(0xff,5);
/* high 8 bits of count-1 */
	immoutb_p(3,5);
/* activate DMA 2 */  /* 开启 DMA 通道2的请求 */
	immoutb_p(0|2,10);
	sti();
}

// 向软驱控制器输出一个字节数据（命令或参数）。
// 实现：
// 在向控制器发送一个字节之前，控制器需要处于准备好状态，并且数据传输方向必须设置成从 CPU 到 FDC，
// 因此函数需要首先读取控制器状态信息。这里使用了循环查询方式，以作适当延时。若出错，则会设置复位标志 reset。
static void output_byte(char byte)
{
	int counter;
	unsigned char status;

	if (reset)
		return;
	// 循环读取主状态控制器 FD_STATUS (0x3f4） 的状态。
	// 如果所读状态是 STATUS_READY 并且方向位 STATUS_DIR = O (CPU->FDC)，则向数据端口输出指定字节。
	// 参考：FDC 主状态寄存器
	for(counter = 0 ; counter < 10000 ; counter++) {
		status = inb_p(FD_STATUS) & (STATUS_READY | STATUS_DIR);
		// status 第7位为1，第6位为0，说明 FDC 数据寄存器已准备就绪，数据传输方向为 CPU 到 FDC
		if (status == STATUS_READY) {
			outb(byte,FD_DATA);
			return;
		}
	}
	// 如果到循环1万次结束还不能发送，则置复位标志，并打印出错信息。
	reset = 1;
	printk("Unable to send byte to FDC\n\r");
}

// 读取 软盘控制器 执行的结果信息。
// 不同命令的返回结果信息长度不一样，最多7个字节，存放在数组 reply_buffer[]中。
// 返回读入的结果字节数，若返回值=-1，则表示出错。程序处理方式与上面函数类似。
// 说明：对于有返回结果的命令可以使用result()函数获取执行结果；对于没有返回结果数据的命令，则应向 FDC 发送检测中断状态命令获得操作的状态：output_byte(FD_SENSEI)。
static int result(void)
{
	int i = 0, counter, status;

	if (reset)
		return -1;
	for (counter = 0 ; counter < 10000 ; counter++) {
		status = inb_p(FD_STATUS)&(STATUS_DIR|STATUS_READY|STATUS_BUSY);
		// 如果控制器状态是 READY，表示已经没有数据可取，则返回己读取的字节数i。
		// 换而言之，如果 status 第7位为1，第6位为0，第4位为0，说明已经发送完数据了，直接返回
		if (status == STATUS_READY)
			return i;
		// 如果控制器状态是方向标志置位(CPU <- FDC)、已准备好、忙，表示有数据可读取。
		// 于是把控制器中的结果数据读入到应答结果数组中。最多读取 MAX_REPLIES(7）个字节。
		// 换而言之，如果 status 第7、6、4位都为1，则开始接收数据。如果直到循环结束也没有收到数据或收到7个以上的数据，就打印出错信息。
		if (status == (STATUS_DIR|STATUS_READY|STATUS_BUSY)) {
			if (i >= MAX_REPLIES)
				break;
			// 从端口读取1个字节（如果fdc中的数据都读取完毕，则STATUS_BUSY复位）
			reply_buffer[i++] = inb_p(FD_DATA);
		}
	}
	// 如果到循环1万次结束时还不能发送，则置复位标志，并打印出错信息。
	reset = 1;
	printk("Getstatus times out\n\r");
	return -1;
}

// 软盘读写出错计数相关处理（出错处理函数）。
	// 该函数根据软盘读写出错次数来确定需要采取的进一步行动：
	// 1、如果当前处理的请求项出错次数大于规定的最大出错次数 MAX_ERRORS（8 次），则不再对当前请求项作进一步的操作尝试。
	// 2、如果读/写出错次数己经超过 MAX_ERRORS/2，则需要对软驱作复位处理，于是设置复位标志 reset。
	// 3、否则若出错次数还不到最大值的一半，则只需重新校正一下磁头位置，于是设置重新校正标志 recalibrate。
	// 真正的复位和重新校正处理会在后续的程序中进行。
static void bad_flp_intr(void)
{
	CURRENT->errors++;
	if (CURRENT->errors > MAX_ERRORS) {
		floppy_deselect(current_drive);
		end_request(0);
	}
	if (CURRENT->errors > MAX_ERRORS/2)
		reset = 1;
	else
		recalibrate = 1;
}	

/*
 * Ok, this interrupt is called after a DMA read/write has succeeded,
 * so we check the results, and copy any buffers.
 * OK，下面的中断处理函数是在 DMA 读/写成功后调用的，这样我们就可以检查执行结果，并复制缓冲区中的数据。
 */
// （DMA读写成功后）软盘读写操作中断调用函数
// 该函数在软驱控制器操作结束后引发的中断处理过程中被调用。
// 实现：
// /**
//  * DMA读写操作后，执行：
//  * 1、检查DMA操作执行结果，出错进行出错处理并重新执行do_fd_request；如未出错，则进行下面操作：
//  * 2、（如需）拷贝临时缓冲区到当前请求项的缓冲区中
//  * 3、结束当前请求项，并处理下一个请求项
//  */
// 函数首先读取操作结果状态信息，据此判断操作是否出现问题并作相应处理。
// 如果读写操作成功，那么若请求项是读操作并且其缓冲区在内存 1MB 以上位置，则需要把数据从软盘临时缓冲区复制到请求项的缓冲区。
static void rw_interrupt(void)
{
	/*
	1、出错处理：
	读取 FDC 执行的结果信息。如果返回结果字节数不等于7，或者状态字节0、1或2中存在出错标志，那么:
	若是处于写保护，就显示出错信息，释放当前驱动器，并结束当前请求项。否则就执行出错计数处理。
	然后继续执行软盘请求项操作。以下状态的含义参见 fdreg.h 文件。

	( 0xf8 = ST0_INTR | ST0_SE | ST0_ECE | ST0_NR )
	( 0xbf = ST1_EOC | ST1_CRC | ST1_OR | ST1_ND | ST1_WP | ST1_MAM，应该是0xb7)
	( 0x73 = ST2_CM | ST2_CRC | ST2_WC | ST2_BC | ST2_MAM )
	 */
	if (result() != 7 || (ST0 & 0xf8) || (ST1 & 0xbf) || (ST2 & 0x73)) {
		if (ST1 & 0x02) {
			printk("Drive %d is write protected\n\r",current_drive);
			floppy_deselect(current_drive);
			end_request(0);
		} else
			bad_flp_intr();
		do_fd_request();
		return;
	}
	// 2、拷贝临时缓冲区到当前请求项的缓冲
	// 如果当前请求项的缓冲区位于 1MB 地址以上，则说明此次软盘读操作的内容还放在临时缓冲区内，
	// 需要复制到当前请求项的缓冲区中（因为 DMA 只能在 1MB 地址范围寻址）
	if (command == FD_READ && (unsigned long)(CURRENT->buffer) >= 0x100000)
		copy_buffer(tmp_floppy_area,CURRENT->buffer);

	// 3、处理下一个请求项
	// 释放当前软驱（取消选定），执行当前请求项结束处理：唤醒等待该请求项的进行，唤醒等待空闲请求项的进程（若有的话），从软驱设备请求项链表中删除本请求项。再继续执行其他软盘请求项操作。
	floppy_deselect(current_drive);
	end_request(1);
	do_fd_request();
}

// 向软盘控制器发送命令和参数，进行软盘DMA读写等操作
// 设置 DMA 通道2，并向软盘控制器输出命令和参数（输出1字节命令+ 0~7字节参数）。
// 若 reset 标志没有置位，那么在该函数退出并且软盘控制器执行完相应读/写操作后就会产生一个软盘中断请求，并开始执行软盘中断处理程序。
static inline void setup_rw_floppy(void)
{
	setup_DMA(); // 设置dma的目标地址
	do_floppy = rw_interrupt;
	// 可参考 表9-20 读扇区数据命令
	output_byte(command);  // 发送命令字节。
	output_byte(head<<2 | current_drive); // 发送参数（磁头号+驱动器号）。
	output_byte(track);
	output_byte(head);
	output_byte(sector);
	output_byte(2);		/* sector size = 512 发送参数(字节数(N=2)512 字节)*/
	output_byte(floppy->sect);
	output_byte(floppy->gap);
	output_byte(0xFF);	/* sector size (0xff when n!=0 ?) */
	// 若上述任何一个 output_byte()操作出错，则会设置复位标志 reset。此时即会立刻去执行 do_fd_request()中的复位处理代码。
	// 如果没有任何出错，则进行DMA
	if (reset)
		do_fd_request();
}

/*
 * This is the routine called after every seek (or recalibrate) interrupt
 * from the floppy controller. Note that the "unexpected interrupt" routine
 * also does a recalibrate, but doesn't come here.
 */

// 寻道中断处理函数
// 调用时机：执行FD_SEEK或FD_RECALIBRATE命令进行寻道处理结束后，中断过程中调用的C函数。
// 实现：
// 首先发送检测中断状态命令，获得状态信息 STO 和磁头所在磁道信息。
// 1、若出错，则执行错误计数检测处理，并取消本次软盘操作请求项。
// 2、若未出错，根据状态信息设置当前磁道变量current_track，然后调用函数 setup_rw_floppy()设置 DMA 并输出软盘读写命令和参数。
static void seek_interrupt(void)
{
/* sense drive status */
	// 首先发送检测中断状态命令，以获取寻道操作执行的结果。该命令不带参数。返回结果信息是两个字节：STO 和 磁头当前磁道号。
	// 然后读取 FDC 执行的结果信息。
	// 如果返回结果字节数不等于2，或者 STO 不为寻道结束（只有ST0位5为1，表示寻道正常结束），或者磁头所在磁道（ST1）不等于设定磁道，则说明发生了错误。
	// 于是执行检测错误计数处理，然后继续执行软盘请求项或执行复位处理。
	/*
	FD_SENSEI 是检测中断状态命令。通常在一个命令执行结束后会向 CPU 发出中断信号后，在中断处理函数中的进行执行。
	对于读写扇区、读写磁道、读写删除标志、读标识场、格式化和扫描等命令以及非 DMA 传输方式下的命令引起的中断，可以直接根据主状态寄存器的标志知道中断原因。
	而对于驱动器就绪信号发生变化、寻道和重新校正（磁头回零道）而引起的中断，由于没有返回结果，就需要利用本命令来读取控制器执行命令后的状态信息。
	*/
	output_byte(FD_SENSEI);
	if (result() != 2 || (ST0 & 0xF8) != 0x20 || ST1 != seek_track) {
		bad_flp_intr();
		do_fd_request();
		return;
	}
	// 若寻道操作成功，则继续执行当前请求项的软盘操作，即向软盘控制器发送命令和参数。
	current_track = ST1;
	setup_rw_floppy();
}

/*
 * This routine is called when everything should be correctly set up
 * for the transfer (ie floppy motor is on and the correct floppy is
 * selected).
 * 该函数是在传输操作的所有信息都正确设置好后被调用的（即软驱马达己开启并且己选择了正确的软盘（软驱）。
 */

// 软盘读写数据传输函数
// 实现：设置驱动器参数后，执行setup_rw_floppy()。
	// 1. 设置磁盘参数（cur_spec1）
	// 2. 设置磁盘数据传输速率（cur_rate）
	// 3. （如需）执行寻道处理：执行 FD_SEEK 或 FD_RECALIBRATE 命令进行寻道处理，结束后触发软盘中断，执行中断处理函数 seek_interrupt() ：
	// 		1. 如果软盘控制器命令执行未出错，则设置当前磁道变量 current_track，并执行 setup_rw_floppy(); 
	// 		2. 如果软盘控制器命令执行出错，则执行错误计数检测处理，并取消本次软盘操作请求项。
	// 4. （如需）执行复位处理，执行 do_fd_request()
	// 5. 执行 setup_rw_floppy()
static void transfer(void)
{
	// 首先检查 当前驱动器参数 是否就是指定驱动器的参数。
	// 若不是就发送设置驱动器参数命令及相应参数（参数1：高4位步进速率，低四位磁头卸载时间；参数2：磁头加载时间）。
	if (cur_spec1 != floppy->spec1) {
		cur_spec1 = floppy->spec1;
		output_byte(FD_SPECIFY);  // 发送设置磁盘参数命令。
		output_byte(cur_spec1);		/* hut etc */
		output_byte(6);			/* Head load time =6ms, DMA */
	}
	// 然后判断 当前数据传输速率 是否与指定驱动器的一致，若不是就发送指定软驱的速率值到数据传输速率控制寄存器 (FD_DCR).
	if (cur_rate != floppy->rate)
		outb_p(cur_rate = floppy->rate,FD_DCR);
	if (reset) {
		do_fd_request();
		return;
	}
	// 如果此时若寻道标志为零（不需要寻道），则设置 DMA 并向软盘控制器发送相应操作命令和参数，然后返回。
	if (!seek) {
		setup_rw_floppy();
		return;
	}
	// 否则就执行寻道处理
	// 于是首先置软盘中断处理调用函数为寻道中断函数。
	do_floppy = seek_interrupt;
	// 开始进行寻道处理：
	// seek_track 是寻道磁道号，可以理解成磁头要移动到的目标磁道号
	// 如果寻道磁道号不等于零，则发送磁头寻道命令和参数。所使用的参数即是第112--121行上设置的全局变量值。
	// 如果寻道磁道号 seek_track 为 0，则执行重新校正命令让磁头归零位。
	if (seek_track) {
		output_byte(FD_SEEK);  // 让选中驱动器的磁头移动到指定磁道上
		output_byte(head<<2 | current_drive);  // 磁头号(位2)、驱动器号（位0-1）
		output_byte(seek_track);  // 磁道号
	} else {
		output_byte(FD_RECALIBRATE);  // 让磁头退回到 0 磁道
		output_byte(head<<2 | current_drive);  // 磁头号(位2)、驱动器号（位0-1）	
	}
	if (reset)
		do_fd_request();
}

/*
 * Special case - used after a unexpected interrupt (or reset)
 */
// 中断处理函数：磁头校正中断
// 1、如果磁头校正失败，则进行软驱复位；否则就就复位recalibrate标识，标识校正成功完成。
// 2、重新执行do_fd_request()
static void recal_interrupt(void)
{
	output_byte(FD_SENSEI);
	if (result()!=2 || (ST0 & 0xE0) == 0x60)
		reset = 1;
	else
		recalibrate = 0;
	do_fd_request();
}

void unexpected_floppy_interrupt(void)
{
	output_byte(FD_SENSEI);
	if (result()!=2 || (ST0 & 0xE0) == 0x60)
		reset = 1;
	else
		recalibrate = 1;
}

// 进行软驱磁头校正
static void recalibrate_floppy(void)
{
	recalibrate = 0;
	current_track = 0;
	do_floppy = recal_interrupt;
	output_byte(FD_RECALIBRATE);
	output_byte(head<<2 | current_drive);
	if (reset)
		do_fd_request();
}

// 中断处理函数：软驱复位中断
// 1、进行设置软驱参数
// 2、重新执行do_fd_request()
static void reset_interrupt(void)
{
	output_byte(FD_SENSEI);
	(void) result();
	output_byte(FD_SPECIFY);
	output_byte(cur_spec1);		/* hut etc */
	output_byte(6);			/* Head load time =6ms, DMA */
	do_fd_request();
}

/*
 * reset is done by pulling bit 2 of DOR low for a while.
 */
// 进行软驱复位
static void reset_floppy(void)
{
	int i;

	reset = 0;
	cur_spec1 = -1;
	cur_rate = -1;
	recalibrate = 1;
	printk("Reset-floppy called\n\r");
	cli();
	do_floppy = reset_interrupt;
	outb_p(current_DOR & ~0x04,FD_DOR);
	for (i=0 ; i<100 ; i++)
		__asm__("nop");
	outb(current_DOR,FD_DOR);
	sti();
}

// 软驱启动正常后的中断处理（由时钟中断（定时器中断）触发）
// 实现：
// 1、首先检查数字输出寄存器(DOR），使其选择当前指定的驱动器。
// 2、调用执行软盘读写传输函数 transfer()。
static void floppy_on_interrupt(void)
{
/* We cannot do a floppy-select, as that might sleep. We just force it */
	selected = 1;
	if (current_drive != (current_DOR & 3)) {
		current_DOR &= 0xFC;
		current_DOR |= current_drive;
		outb(current_DOR,FD_DOR);
		add_timer(2,&transfer);
	} else
		// 软盘读写传输
		transfer();
}

// 处理软盘读写请求（处理当前请求项）
// 1、处理复位和重新校正
// 2、设置当前请求项中的所需的信息（例如C、H、S、读写指令command等）
// 3、然后添加计时器执行floppy_on_interrupt()
void do_fd_request(void)
{
	unsigned int block;

	seek = 0;
	// 1、处理复位和重新校正
	if (reset) {
		reset_floppy();
		return;
	}
	if (recalibrate) {
		recalibrate_floppy();
		return;
	}

	INIT_REQUEST;
	// floppy_struct
	floppy = (MINOR(CURRENT->dev)>>2) + floppy_type;
	if (current_drive != CURRENT_DEV)
		seek = 1;
	current_drive = CURRENT_DEV;
	// 设置读写起始扇区 block。因为每次读写是以块为单位（1块为2 个扇区)，所以起始扇区需要起码比磁盘总扇区数小 2 个扇区。
	// 否则说明这个请求项参数无效，结束该次软盘请求项去执行下一个请求项。
	block = CURRENT->sector;
	if (block+2 > floppy->size) {
		end_request(0);
		goto repeat;
	}

	// 2、计算CHS
	// 起始扇区的绝对扇区号 / 每磁道扇区数
	sector = block % floppy->sect; // 磁道上扇区号（S）
	block /= floppy->sect;         // 起始扇区的绝对磁道号
	// 总磁道数 / 磁头数
	head = block % floppy->head;  // 扇区对应的磁头号（H）
	track = block / floppy->head; // 磁道号（C，柱面号） 在许多场合，磁道和柱面可以互换使用；https://blog.csdn.net/hengfeng430/article/details/135412133
	// 根据不同软驱中盘类型进行调整，得出搜索磁道号
	seek_track = track << floppy->stretch;
	// 是否重新寻道
	if (seek_track != current_track)
		seek = 1;
	sector++; // 磁道上的扇区从1号开始计数

    // 设置操作命令
	if (CURRENT->cmd == READ)
		command = FD_READ;
	else if (CURRENT->cmd == WRITE)
		command = FD_WRITE;
	else
		panic("do_fd_request: unknown command");

	// 3、添加计时器，等待马达启动后，执行floppy_on_interrupt
	add_timer(ticks_to_floppy_on(current_drive),&floppy_on_interrupt);
}

void floppy_init(void)
{
	blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
	set_trap_gate(0x26,&floppy_interrupt);  // 设置idt表项
	outb(inb_p(0x21)&~0x40,0x21);  // 复位软盘中断请求屏蔽位。
}
